/**
 * License would add here
 */

#ifndef UTILS_JS_VALUE_BASE_H
#define UTILS_JS_VALUE_BASE_H

#include <cmath>
#include <csignal>
#include <cstring>

#ifndef OHOS_PLATFORM
#include <node_api.h>
#else
#include <napi/native_api.h>
#endif

#include <ostream>

#include "macros.h"

class JSUndefined;
class JSNull;
class JSBoolean;
class JSNumber;
class JSString;
class JSObject;

class NonAllocable {
private:
    [[noreturn]] void* operator new(size_t)
    {
        THROW_EXCEPTION("this class is not allocable");
        std::abort();
    }
    void operator delete(void*) { }
};

class JSValue : public NonAllocable {
public:
    JSValue(const napi_env& env)
        : env_(env) {};
    JSValue(const JSValue& value)
        : env_(value.env_)
        , value_(value.value_)
        , type_(value.type_)
    {
        UpdateType();
    };
    JSValue(const napi_env& env, const napi_value& value)
        : env_(env)
        , value_(value)
    {
        UpdateType();
    };

    inline void UpdateType() { type_ = Typeof(env_, value_); }
    inline napi_valuetype GetType() const { return type_; }

    inline napi_value GetRawData() const { return value_; }

    inline int operator=(const napi_value& value)
    {
        SetValue(value);
        return 0;
    }

    inline JSValue& operator=(const JSValue& value)
    {
        value_ = value.GetRawData();
        type_ = value.GetType();
        return *this;
    }

    inline napi_value* operator&() { return &value_; }

    inline operator napi_value() const { return value_; };
    inline operator napi_ref()
    {
        napi_ref ref = nullptr;
        napi_create_reference(env_, value_, 1, &ref);
        return ref;
    };

    inline bool Equals(JSValue value)
    {
        bool result = false;
        napi_strict_equals(env_, value_, value, &result);
        return result;
    }
    inline bool Equals(napi_value value)
    {
        bool result = false;
        napi_strict_equals(env_, value_, value, &result);
        return result;
    }
    bool operator==(napi_value value) { return Equals(value); }
    bool operator==(JSValue value) { return Equals(value); }
    bool operator!=(napi_value value) { return !Equals(value); }
    bool operator!=(JSValue value) { return !Equals(value); }

    template <class T>
    inline T const As() const
    {
        return T(*this);
    };

#define BASE_IS_TYPE(method, type) \
    inline bool Is##method() const { return type_ == napi_##type; }

    BASE_IS_TYPE(Undefined, undefined)
    BASE_IS_TYPE(Null, null)
    BASE_IS_TYPE(Boolean, boolean)
    BASE_IS_TYPE(Number, number)
    BASE_IS_TYPE(String, string)
    BASE_IS_TYPE(Symbol, symbol)
    BASE_IS_TYPE(Object, object)
    BASE_IS_TYPE(Function, function)
    BASE_IS_TYPE(External, external)
    BASE_IS_TYPE(BigInt, bigint)

#undef BASE_IS_TYPE

#define EXTRA_IS_TYPE(method, type)            \
    inline bool Is##method() const             \
    {                                          \
        bool result = false;                   \
        napi_is_##type(env_, value_, &result); \
        return result;                         \
    }

    EXTRA_IS_TYPE(Array, array)
    EXTRA_IS_TYPE(Arraybuffer, arraybuffer)
    EXTRA_IS_TYPE(Buffer, buffer)
    EXTRA_IS_TYPE(Date, date)
    EXTRA_IS_TYPE(Error, error)
    EXTRA_IS_TYPE(Typedarray, typedarray)
    EXTRA_IS_TYPE(Dataview, dataview)
    EXTRA_IS_TYPE(DetachedArraybuffer, detached_arraybuffer)
    EXTRA_IS_TYPE(Promise, promise)

#undef EXTRA_IS_TYPE

    inline JSBoolean ToBoolean() const;
    inline JSNumber ToNumber() const;
    inline JSObject ToObject() const;
    inline JSString ToString() const;

    inline static napi_valuetype Typeof(napi_env env, napi_value value)
    {
        napi_valuetype type = napi_undefined;
        napi_typeof(env, value, &type);
        return type;
    }

    inline static napi_valuetype Typeof(const JSValue& value)
    {
        return Typeof(value.env_, value);
    }

    inline JSUndefined GetUndefined();
    inline JSNull GetNull();
    inline JSBoolean GetBoolean(bool value);
    inline JSObject GetGlobal();

    inline std::ostream& operator<<(std::ostream& out);
    friend inline std::ostream& operator<<(std::ostream& out,
        const JSValue& value)
    {
        napi_env env = value.env_;
        JSValue string(env);
        napi_coerce_to_string(env, value, &string);
        size_t length = 0;
        napi_get_value_string_utf8(env, string, nullptr, 0, &length);
        std::string result(length + 1, 0);
        napi_get_value_string_utf8(
            env, string, const_cast<char*>(result.data()), result.length(), &length);

        out << result.c_str();
        return out;
    };

    inline static JSUndefined GetUndefined(napi_env env);
    inline static JSNull GetNull(napi_env env);
    inline static JSBoolean GetBoolean(napi_env env, bool value);
    inline static JSObject GetGlobal(const napi_env& env);

protected:
    inline void SetValue(const napi_value& value)
    {
        value_ = value;
        UpdateType();
    }

    inline napi_env GetEnv() const { return env_; }
    inline napi_value GetValue() const { return value_; }

    inline static napi_env GetEnv(const JSValue& value) { return value.env_; }
    inline static napi_value GetValue(const JSValue& value) { return value.value_; }

    const napi_env env_ { nullptr };
    napi_value value_ { nullptr };
    napi_valuetype type_ { napi_undefined };

    friend class JSValueRef;
    friend class JSValueWrapper;
};

class JSUndefined : public JSValue {
public:
    JSUndefined(napi_env env)
        : JSValue(env)
    {
        napi_get_undefined(env, &value_);
        UpdateType();
    }
    JSUndefined(const JSValue& value)
        : JSValue(value)
    {
        if (!IsUndefined()) {
            THROW_EXCEPTION("undefined expected, but " << ReadNapiType(type_)
                                                       << "is provided");
        }
    }

    operator uintptr_t() { return 0; }

private:
};

inline JSUndefined JSValue::GetUndefined(napi_env env)
{
    return JSUndefined(env);
}

inline JSUndefined JSValue::GetUndefined()
{
    return JSUndefined(env_);
}

class JSNull : public JSValue {
public:
    JSNull(napi_env env)
        : JSValue(env)
    {
        napi_get_null(env, &value_);
        UpdateType();
    }
    JSNull(const JSValue& value)
        : JSValue(value)
    {
        if (!IsNull()) {
            THROW_EXCEPTION("null expected, but " << ReadNapiType(type_)
                                                  << "is provided");
        }
    }
    operator uintptr_t() { return 0; }

private:
};

inline JSNull JSValue::GetNull(napi_env env)
{
    return JSNull(env);
}

inline JSNull JSValue::GetNull()
{
    return JSNull(env_);
}

class JSBoolean : public JSValue {
public:
    JSBoolean(napi_env env, bool value)
        : JSValue(env)
    {
        napi_get_boolean(env, value, &value_);
        UpdateType();
    }
    JSBoolean(const JSValue& value)
        : JSValue(value)
    {
        if (!IsBoolean()) {
            THROW_EXCEPTION("boolean expected, but " << ReadNapiType(type_)
                                                     << "is provided");
        }
    }

    inline JSBoolean operator=(bool value)
    {
        napi_get_boolean(env_, value, &value_);
        return *this;
    }

private:
};

inline JSBoolean JSValue::ToBoolean() const
{
    JSValue result(env_);
    napi_coerce_to_bool(env_, value_, &result);
    return result;
}

inline JSBoolean JSValue::GetBoolean(napi_env env, bool value)
{
    return JSBoolean(env, value);
}

inline JSBoolean JSValue::GetBoolean(bool value)
{
    return JSBoolean(env_, value);
}

#endif /* UTILS_JS_VALUE_BASE_H */
